《深入理解Java虚拟机》读书笔记 - Java内存模型与线程
此篇为《深入理解Java虚拟机》第十二章12.3、12.4部分的读书笔记
Java 内存模型
Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台上都能达到一致的内存访问效果。
定义 Java 内存模型并非一件容易的事情:这个模型必须定义得足够严谨,才能让 Java 的并发内存访问操作不会产生歧义;但是,也必须定义得足够宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器,高速缓冲和指令集中某些特有的指令)来获得更好的执行速度。
主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效率,Java 内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java 内存模型规定了所有的变量都存储在主内存中(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者关系图如下所示。
这部分中与前面笔记中所记录的 Java 内存区域并不是同一层次的内存划分,两者基本上没有什么关系。
内存间互相操作
这一部分中, Java 内存模型定义了8种操作来完成主内存与工作内存之间的具体变量交互工作:lock(锁定)、unlock(解锁)、 read(读取)、load(载入)、 use(使用)、assign( 赋值)、 store(存储)、 write( 写入),虚拟机实现时必须保证以上所有操作都是原子的、不可再分的。
具体操作实现在P364。
对于 volatile 型变量的特殊规则
关键字 volatile 是 Java 虚拟机所提供的轻量级同步机制。当一个变量定义为 volatile 之后,它将具备两种特性:
保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递需要通过主内存来完成。例如:线程 A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。
由于 volatile 变量只能保证可见性,而无法保证操作的原子性。在不符合以下两条规则的运算场景下,我们仍然需要加锁机制来保证操作原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程来修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束。
禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作顺序与代码执行顺序一致。
在于 volatile 的性能提升方面,可得出:volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏蔽指令保证处理器不发生乱序执行。不过即使如此,大多数场景下 volatile 的总开销仍然要比锁低,我们在 volatile 与锁之间的选择依据只取决于 volatile 是否能够满足当前场景所需的并发要求。
先行发生原则
如果 Java 内存模型中所有的有序性都仅仅依靠 volatile 和 synchronized 来完成,那么有一些操作将会变得很烦琐,但是在实际编程当中,Java 语言内部中存在一种“先行发生”(happens-before)的原则来保证代码在正常情况下的并发处理。
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响”包括了修改了内存中共享变量的值、发送了消息、调用了方法等。
下面是 Java 内存模型中一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码,因为需要考虑分支、循环等结构。
- 管程锁定规则(Monitor Lock Rule): 一个 unlock 操作先行发生于后面(特指时间顺序)对同一个锁的 lock 操作。
- volatile 变量规则(Volatile Variable Rule): 对一个 volatile 变量的写操作先行发生于后面(特指时间顺序)对这个变量的读操作。
- 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule): 线程中的所有操作都先行发生于对此线程的终止检测。
- 线程中断规则(Thread Interruption Rule): 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 对象终结规则(Finalizer Rule): 一个对象的初始化完成(构造函数的结束)先行发生于它的 finalize() 方法的开始。
- 传递性( Transitivity): 如果操作 A 先行发生于操作 B,而操作 B 先行发生于操作 C,那就可以推导出操作 A 先行发生于操作 C 的结论。
Java 与线程
线程创建&线程调度
P379,与操作系统创建线程相同,因为 Java 线程创建与线程调度的操作就是基于操作系统的。
状态转换
新建(New):创建后尚未未启动的线程。
运行(Runable):线程有可能正在运行,或也有可能正在等待CPU为它分配执行时间。
无限期等待(Waiting):不会被分配CPU执行时间,要等待被其他线程显式唤醒,以下方法会让线程处于无限期的等待状态:
- 没有设置 Timeout 参数的 Object.wait() 方法。
- 没有设置 Timeout 参数的 Thread.join() 方法。
- LockSupport.park() 方法。
限期等待(Timed Waiting):不会被分配CPU执行时间,不需要等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒,以下方法会让线程处于限期的等待状态:
- Thread.sleep() 方法。
- 设置了 Timeout 参数的Object.wait() 方法。
- 设置了 Timeout 参数的Thread.join() 方法。
- LockSupport.parkNanos() 方法。
- LockSupport.parkUntil() 方法。
阻塞(Blocked):线程被阻塞了,在等待获取一个排它锁。例如线程A和B在执行同步方法C时,线程A先拿到排它锁,那么线程B的状态就是阻塞状态,等待线程B释放排它锁。
结束(Terminated):线程执行完毕。